﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="FlyingTabs.cs" company="none">
//
// FlyingTabs, a .NET library to move tab pages in an application.
// Copyright (C) 2012 nant-cross-compile team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see http://www.gnu.org/licenses/. 
// </copyright>
// <author>
//   <name>Roland Reinl</name>
//   <mail>reinlroland@googlemail.com</mail>
// </author>
// <history>
//   <historyitem date="2012-07-04" change="Created file"/>
//   <historyitem date="2012-11-16" author="Roland Reinl" change="Code cleanup"/>
// </history>
// 
// --------------------------------------------------------------------------------------------------------------------
namespace FlyingTabsLib
{
  using System.Collections.Generic;
#if DEBUG
  using System.Diagnostics;
#endif
  using System.Drawing;
  using System.Windows.Forms;

  /// <summary>
  /// Extension class for moving tab pages in and between tab controls.
  /// </summary>
  public static class FlyingTabs
  {
    /// <summary>
    /// A list with all registered <see cref="TabControl"/> controls.
    /// </summary>
    private static readonly List<TabControl> registeredTabControls = new List<TabControl>();

    /// <summary>
    /// The currently moved <see cref="TabPage"/> instance.
    /// </summary>
    private static TabPageInfo sourceTabPage;

    /// <summary>
    /// Gets the tab page at cursor position.
    /// </summary>
    private static TabPage TabPageAtCursorPosition
    {
      get
      {
        foreach (TabControl tc in registeredTabControls)
        {
          if (tc.FindForm() == Form.ActiveForm)
          {
            for (int i = 0; i < tc.TabPages.Count; i++)
            {
              if (tc.GetTabRect(i).Contains(tc.PointToClient(Cursor.Position)) == true)
              {
                return tc.TabPages[i];
              }
            }
          }
        }

        return null;
      }
    }

    /// <summary>
    /// Enables drag and drop tabs.
    /// </summary>
    /// <param name="tabControl">The tab control for which drag and drop will be enabled.</param>
    public static void EnableDragAndDropTabs(this TabControl tabControl)
    {
      registeredTabControls.Add(tabControl);
      tabControl.AllowDrop = true;
      tabControl.MouseDown += HandleMouseDownEvent;
      tabControl.DragEnter += HandleDragEnterEvent;
      tabControl.QueryContinueDrag += HandleQueryContinueDragEvent;
#if DEBUG
      Debug.WriteLine(string.Format("Enabling Drag and Drop for TabControl {0}", tabControl.Name));
#endif
    }

    /// <summary>
    /// Disables drag and drop tabs.
    /// </summary>
    /// <param name="tabControl">The tab control for which drag and drop will be enabled.</param>
    public static void DisableDragAndDropTabs(this TabControl tabControl)
    {
      tabControl.AllowDrop = false;
      tabControl.MouseDown -= HandleMouseDownEvent;
      tabControl.DragEnter -= HandleDragEnterEvent;
      tabControl.QueryContinueDrag -= HandleQueryContinueDragEvent;
      registeredTabControls.Remove(tabControl);
#if DEBUG
      Debug.WriteLine(string.Format("Disabling Drag and Drop for TabControl {0}", tabControl.Name));
#endif
    }

    /// <summary>
    /// Handles the DragEnter event of the <see cref="TabControl"/> control.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.Windows.Forms.DragEventArgs"/> instance containing the event data.</param>
    private static void HandleDragEnterEvent(object sender, DragEventArgs e)
    {
      TabControl tabControl = sender as TabControl;
#if DEBUG
      Debug.WriteLine(string.Format("Handling DragEnter event for TabControl {0}", tabControl.Name));
#endif

      // Dropping data is only allowd if dropped data contains a TabPage and no cirular reference can occour
      if (e.Data.GetDataPresent(typeof(TabPage)) && (IsCircularReference(sourceTabPage.TabPage, tabControl) == false))
      {
        e.Effect = DragDropEffects.Move;
        
        // Activate inactive form to simplify drag and drop between multiple forms
        if (tabControl.FindForm() != Form.ActiveForm)
        {
          tabControl.FindForm().Activate();
        }
      }
      else
      {
        e.Effect = DragDropEffects.None;
      }
    }

    /// <summary>
    /// Handles the MouseDown event of the <see cref="TabControl"/> control.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.Windows.Forms.MouseEventArgs"/> instance containing the event data.</param>
    private static void HandleMouseDownEvent(object sender, MouseEventArgs e)
    {
      if (e.Button == System.Windows.Forms.MouseButtons.Left)
      {
        TabControl tabControl = sender as TabControl;
#if DEBUG
        Debug.WriteLine(string.Format("Handling MouseDown event for TabControl {0}", tabControl.Name));
#endif
        sourceTabPage = new TabPageInfo(tabControl.SelectedTab);
        tabControl.DoDragDrop(tabControl.SelectedTab, DragDropEffects.Move);
      }
    }

    /// <summary>
    /// Handles the QueryContinueDrag event of the <see cref="TabControl"/> control.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.Windows.Forms.QueryContinueDragEventArgs"/> instance containing the event data.</param>
    private static void HandleQueryContinueDragEvent(object sender, QueryContinueDragEventArgs e)
    {
      TabControl tabControl = sender as TabControl;
#if DEBUG
      Debug.WriteLine(string.Format("Handling QueryContinueDrag event for TabControl {0}", tabControl.Name));
#endif

      if (e.EscapePressed == true)
      {
        sourceTabPage.RevertMovement();
        e.Action = DragAction.Cancel;
        return;
      }

      TabPage targetTabPage = TabPageAtCursorPosition;
#if DEBUG
      if (targetTabPage != null)
      {
        Debug.WriteLine(string.Format("Target TabPage: {0}", targetTabPage.Name));
      }
      else
      {
        Debug.WriteLine("Found no TabPage");
      }
#endif
      if ((targetTabPage != null) && (sourceTabPage.TabPage != targetTabPage))
      {
        TabControl targetTabControl = targetTabPage.Parent as TabControl;

        if (IsCircularReference(sourceTabPage.TabPage, targetTabControl) == true)
        {
          return;
        }

        if (targetTabControl.TabPages.Contains(sourceTabPage.TabPage) == true)
        {
          // Tab control already contains tab page, just swap it
          int sourceIndex = targetTabControl.TabPages.IndexOf(sourceTabPage.TabPage);
          int targetIndex = targetTabControl.TabPages.IndexOf(targetTabPage);
          targetTabControl.TabPages[sourceIndex] = targetTabPage;
          targetTabControl.TabPages[targetIndex] = sourceTabPage.TabPage;
        }
        else
        {
          // Tab control doesn't contain tab page, insert it
          targetTabControl.TabPages.Insert(targetTabControl.TabPages.IndexOf(targetTabPage), sourceTabPage.TabPage);
        }

        targetTabControl.SelectedTab = sourceTabPage.TabPage;
      }
    }

    /// <summary>
    /// Determines whether a reference would be a circular reference.
    /// E.g. if <paramref name="targetTabControl"/> is a child element of the <paramref name="sourceTabPage"/>.
    /// </summary>
    /// <param name="sourceTabPage">The source tab page.</param>
    /// <param name="targetTabControl">The target tab control.</param>
    /// <returns>
    ///   <c>true</c> if a reference would be a circular reference; otherwise, <c>false</c>.
    /// </returns>
    private static bool IsCircularReference(TabPage sourceTabPage, TabControl targetTabControl)
    {
      Control parentControl = targetTabControl.Parent;
      while (parentControl != null)
      {
        if (parentControl == sourceTabPage)
        {
          return true;
        }

        parentControl = parentControl.Parent;
      }

      return false;
    }
  }
}
